package org.third.integration.etcd.imp;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.third.common.SS_CONSTANT;
import org.third.common.utils.EndpointUtils;
import org.third.common.utils.EnvUtils;
import org.third.common.utils.HttpClientUtils;
import org.third.integration.etcd.KubeEtcdClient;
import org.third.integration.etcd.bean.KubeEtcdResponseV3;
import org.third.integration.etcd.utils.EtcdWathcer;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class KubeEtcdClientImp implements KubeEtcdClient{
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private static final String KEY_NOT_FOUND = "100";
	private static final String NOT_A_DIRECTORY = "104";
	
	@Autowired
	private ObjectMapper mapper;
	
	@Autowired
	private EtcdWathcer etcdWathcer;
	
	private CloseableHttpClient httpclient = HttpClientUtils.getCertificateIngnoredAndWithClientCertificateHttpClient(
			EnvUtils.getEtcdclientcertificatefile(), EnvUtils.getEtcdclientcertificatepwd());
	
	private String url = EndpointUtils.getEtcdendpoint() + SS_CONSTANT.ETCD_BASE_URL;
	private String postUrl = EndpointUtils.getEtcdendpoint() +SS_CONSTANT.POST_ETCD_BASE_URL;
	private String deleteUrl = EndpointUtils.getEtcdendpoint() + SS_CONSTANT.DELETE_ETCD_BASE_URL;

	@Override
	public void put(String key, String value){
		if(StringUtils.isEmpty(key)){
			throw new IllegalArgumentException("key can't be null");
		}
		String requestBody = null;

		//add base64 encode to key and value
		if(StringUtils.isEmpty(value)){
			try{
				key = new String(Base64.getEncoder().encode(key.getBytes("UTF-8")),"UTF-8");

			}catch (UnsupportedEncodingException e){
				String errMsg = String.format("Failed to encode key [%s]", key);
				throw new RuntimeException(errMsg);
			}

		}else{
			try{
				key = new String(Base64.getEncoder().encode(key.getBytes("UTF-8")),"UTF-8");
				value = new String(Base64.getEncoder().encode(value.getBytes("UTF-8")),"UTF-8");

			}catch (UnsupportedEncodingException e){
				String errMsg = String.format("Failed to encode key [%s] value [%s]", key, value);
				throw new RuntimeException(errMsg);
			}
		}

		requestBody = "{\"key\": \""+key+"\", \"value\": \""+value+"\"}";
		CloseableHttpResponse response = null;
		try {
			HttpPost HttpPost = new HttpPost(this.postUrl);
			HttpPost.addHeader("Content-type", "application/json");
			HttpPost.addHeader("Accept", "application/json");
			HttpPost.addHeader("Authorization", SS_CONSTANT.IDM_BASIC_AUTH);
			try {

				HttpPost.setEntity(new ByteArrayEntity(requestBody.getBytes("UTF-8")));

			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			response = httpclient.execute(HttpPost);
			logger.info("response.getStatusLine().getStatusCode():"+response.getStatusLine().getStatusCode());
			if(response.getStatusLine().getStatusCode() != 200 && response.getStatusLine().getStatusCode() != 201){
			    HttpEntity entity = response.getEntity();
			    String content = IOUtils.toString(entity.getContent(), "UTF-8");
			    String errMsg = String.format("%s%s%s", response.getStatusLine().toString(), System.lineSeparator(), content);
			    throw new RuntimeException(errMsg);
			}
		} catch (Exception e) {
			String errMsg = String.format("Failed to store key [%s] value [%s]", key, value);  
			throw new RuntimeException(errMsg, e);
		}finally {
			if(response != null){
				try {
					response.close();
				} catch (IOException e) {
					logger.warn("something wrong when closing response.", e);
				}
			}
		}
	}

	@Override
	public String get(String key) {
		return this.get(key, false);
	}
	
	@Override
	//if key not found, return null.
	public String get(String key, boolean wait){
		if(StringUtils.isEmpty(key)){
			throw new IllegalArgumentException("key can't be null");
		}
		//add base64 encode to key when get value by key
        logger.info("key:"+key);
		try {
			key = new String(Base64.getEncoder().encode(key.getBytes("UTF-8")),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			String errMsg = String.format("Failed to encode key [%s]", key);
			throw new RuntimeException(errMsg);
		}

		StringBuffer responseBody = requestValue(key, wait);
		logger.info("Base64-key:"+key);
		logger.info("responseBody:"+responseBody.toString() );
		KubeEtcdResponseV3 etcdResponse1 = null;
		String value = null;
		try {

			etcdResponse1 = mapper.readValue(responseBody.toString(), KubeEtcdResponseV3.class);

		} catch (Exception e) {
			String errMsg = String.format("Failed to mapping response to class EtcdResponse, response: %s", responseBody);
			throw new RuntimeException(errMsg);
		}
		if(etcdResponse1.getKvs()!=null&&etcdResponse1.getKvs().size()>0 ){
			value = etcdResponse1.getKvs().get(0).getValue();
			if(StringUtils.isEmpty(value)){
				logger.info("value not exist");
				value = "";
				return value;
			}else if(value.equals("null")){
                logger.info("value is null");
			    value = null;
			    return value;
            } else{
				try {
					value = new String(Base64.getDecoder().decode(value),"UTF-8");
				} catch (UnsupportedEncodingException e) {
					String errMsg = String.format("Failed to decode value [%s]", value);
					throw new RuntimeException(errMsg);
				}
			}
		}else {
			logger.warn("can't get value from key %s, Etcd response: %s.", key, etcdResponse1.toString());
		}

		return value;
	}

	//return the response body directly not matter what the status code is.
	private StringBuffer requestValue(String key, boolean wait){
		StringBuffer responseBody = null;
		String requestBody = null;
		requestBody = "{\"key\": \""+key+"\"}";
		logger.info("requestBody is ============="+requestBody);

//		String fullUrl = this.url+key;
//		if(wait){
//			fullUrl = fullUrl + "?wait=true";
//			return etcdWathcer.watchEtcdValue(fullUrl);
//		}
		HttpPost httpPost = new HttpPost(this.url);
		httpPost.addHeader("Content-type", "application/json");
		httpPost.addHeader("Accept", "application/json");
		httpPost.addHeader("Authorization", SS_CONSTANT.IDM_BASIC_AUTH);
		try {

			httpPost.setEntity(new ByteArrayEntity(requestBody.getBytes("UTF-8")));

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		CloseableHttpResponse response = null;
		try {
			response = httpclient.execute(httpPost);
		    HttpEntity entity = response.getEntity();
			logger.info("response StatusCode():"+response.getStatusLine().getStatusCode());

		    responseBody = new StringBuffer(IOUtils.toString(entity.getContent(), "UTF-8"));
		    EntityUtils.consume(entity);
		} catch (Exception e) {
			String errMsg = String.format("Failed to request key [%s]", key);
			throw new RuntimeException(errMsg, e);
		}finally {
			if(response != null){
				try {
					response.close();
				} catch (IOException e) {
					logger.warn("something wrong when closing response.", e);
				}
			}
		}
		return responseBody;
	}
	
	@Override
	//if dir is not found, empty String[] will be returned.
	public String[] ls(String dir){
		if(StringUtils.isEmpty(dir)){
			throw new IllegalArgumentException("dir can't be null");
		}
        String key = null;
		List<String> results = new ArrayList<String>();
        for(KubeEtcdResponseV3.Kvs kvs:getForRange(dir)){
            //decode key and value
            try {
                key = new String(Base64.getDecoder().decode(kvs.getKey()),"UTF-8");
            } catch (UnsupportedEncodingException e) {
                String errMsg = String.format("Failed to decode key [%s] value [%s]", key);
                throw new RuntimeException(errMsg);
            }
            results.add(key);
        }

		return results.toArray(new String[0]);
	}

	@Override
	public void delete(String key) {
		if(StringUtils.isEmpty(key)){
			throw new IllegalArgumentException("key can't be null");
		}

		//encode key when delete value by key
		try {
			key = new String(Base64.getEncoder().encode(key.getBytes("UTF-8")),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			String errMsg = String.format("Failed to encode key [%s]", key);
			throw new RuntimeException(errMsg);
		}

		String requestBody = null;
		requestBody = "{\"key\": \""+key+"\"}";
		HttpPost httpDelete = new HttpPost(this.deleteUrl);
		httpDelete.addHeader("Content-type", "application/json");
		httpDelete.addHeader("Accept", "application/json");
		httpDelete.addHeader("Authorization", SS_CONSTANT.IDM_BASIC_AUTH);
		try {

			httpDelete.setEntity(new ByteArrayEntity(requestBody.getBytes("UTF-8")));

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		CloseableHttpResponse response = null;
		try {
			response = httpclient.execute(httpDelete);
		    HttpEntity entity = response.getEntity();
		    StringBuffer responseBody = new StringBuffer(IOUtils.toString(entity.getContent(), "UTF-8"));
			logger.info("response.getStatusLine().getStatusCode():"+response.getStatusLine().getStatusCode());
			logger.info("delete key is:"+requestBody);
		    EntityUtils.consume(entity);
		} catch (Exception e) {
			String errMsg = String.format("Failed to delete key [%s]", key);  
			throw new RuntimeException(errMsg, e);
		}finally {
			if(response != null){
				try {
					response.close();
				} catch (IOException e) {
					logger.warn("something wrong when closing response.", e);
				}
			}
		}
	}

	@Override
	public Map<String,String> range(String dir){
		if(StringUtils.isEmpty(dir)){
			throw new IllegalArgumentException("dir can't be null");
		}
		String key = null;
		String value = null;

		Map<String,String> etcdKVMap = new HashMap<>();

		for(KubeEtcdResponseV3.Kvs kvs:getForRange(dir)){
				//decode key and value
				try {
					key = new String(Base64.getDecoder().decode(kvs.getKey()),"UTF-8");
					value = new String(Base64.getDecoder().decode(kvs.getValue()),"UTF-8");
				} catch (UnsupportedEncodingException e) {
					String errMsg = String.format("Failed to decode key [%s] value [%s]", key, value);
					throw new RuntimeException(errMsg);
				}
				etcdKVMap.put(key,value);

		}

		return etcdKVMap;

	}

	//get all etcd value and key list
	private List<KubeEtcdResponseV3.Kvs> getForRange(String dir) {
		List<KubeEtcdResponseV3.Kvs> kvsList = new ArrayList<>();
		StringBuffer responseBody = requestValueForRange(dir);
		logger.info("responseBody:"+responseBody.toString() );
		KubeEtcdResponseV3 etcdResponse1 = null;
		try {

			etcdResponse1 = mapper.readValue(responseBody.toString(), KubeEtcdResponseV3.class);

		} catch (Exception e) {
			String errMsg = String.format("Failed to mapping response to class EtcdResponse, response: %s", responseBody);
			throw new RuntimeException(errMsg);
		}
		if(etcdResponse1.getKvs()!=null&&etcdResponse1.getKvs().size()>0 ){
			logger.info("allListSize==="+etcdResponse1.getKvs().size());
			return etcdResponse1.getKvs();
		}else {
			logger.warn("can't get value from key %s, Etcd response:", "", etcdResponse1.toString());
		}
		return kvsList;
	}


	//return the response body directly not matter what the status code is.
	private StringBuffer requestValueForRange(String dir) {
		//String key = "\\\\0";
		//get range_end
		String key = dir;
		String lastIndex = key.substring(key.length()-1, key.length());
		String asc = this.stringToAscii(lastIndex);
		int lagerAsc = Integer.parseInt(asc) + 1;
		String end = this.asciiToString(lagerAsc+"");
		String range_end = key.substring(0,key.length()-1).concat(end);

		logger.info("key===="+dir+"--------"+"range_key===="+range_end);
		try {
			key = new String(Base64.getEncoder().encode(key.getBytes("UTF-8")),"UTF-8");
			range_end = new String(Base64.getEncoder().encode(range_end.getBytes("UTF-8")),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			String errMsg = String.format("Failed to encode key [%s]", key);
			throw new RuntimeException(errMsg);
		}

		StringBuffer responseBody = null;

		String requestBody = null;

		requestBody = "{\"key\":\""+key+"\",\"range_end\":\""+range_end+"\"}";

		logger.info("requestBody======="+requestBody);

		HttpPost httpPost = new HttpPost(this.url);
		httpPost.addHeader("Content-type", "application/json");
		httpPost.addHeader("Accept", "application/json");
		httpPost.addHeader("Authorization", SS_CONSTANT.IDM_BASIC_AUTH);


		try {

			httpPost.setEntity(new ByteArrayEntity(requestBody.getBytes("UTF-8")));

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		CloseableHttpResponse response = null;
		CloseableHttpResponse response1 = null;
		try {
			response = httpclient.execute(httpPost);
			HttpEntity entity = response.getEntity();
			logger.info("response StatusCode():"+response.getStatusLine().getStatusCode());

			responseBody = new StringBuffer(IOUtils.toString(entity.getContent(), "UTF-8"));
			EntityUtils.consume(entity);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}finally {
			if(response != null){
				try {
					response.close();
				} catch (IOException e) {
					logger.warn("something wrong when closing response.", e);
				}
			}
		}
		return responseBody;
	}


	//ascII to String
	private  String stringToAscii(String value)
	{
		StringBuffer sbu = new StringBuffer();
		char[] chars = value.toCharArray();
		for (int i = 0; i < chars.length; i++) {
			if(i != chars.length - 1)
			{
				sbu.append((int)chars[i]).append(",");
			}
			else {
				sbu.append((int)chars[i]);
			}
		}
		return sbu.toString();
	}


	//String to ascII
	private  String asciiToString(String value)
	{
		StringBuffer sbu = new StringBuffer();
		String[] chars = value.split(",");
		for (int i = 0; i < chars.length; i++) {
			sbu.append((char) Integer.parseInt(chars[i]));
		}
		return sbu.toString();
	}


}
